To be sure, the process of building a complex .NET application in CIL would be quite the labor of love. On the one hand, CIL is an extremely expressive programming language that allows you to interact with all of the programming constructs allowed by the CTS. On the other hand, authoring raw CIL is tedious, error-prone, and painful. While it is true that knowledge is power, you may indeed wonder just how important it is to commit the laws of CIL syntax to memory. The answer is, “It depends.” To be sure, most of your .NET programming endeavors will not require you to view, edit, or author CIL code. However, with the CIL primer behind you, you are now ready to investigate the world of dynamic assemblies (as opposed to static assemblies) and the role of the System.Reflection.Emit namespace.
The first question you may have is, “What exactly is the difference between static and dynamic assemblies?” By definition, static assemblies are .NET binaries loaded directly from disk storage, meaning they are located somewhere on your hard drive in a physical file (or possibly a set of files in the case of a multifile assembly) at the time the CLR requests them. As you might guess, every time you compile your C# source code, you end up with a static assembly.
A dynamic assembly, on the other hand, is created in memory on the fly using the types provided by the System.Reflection.Emit namespace. The System.Reflection.Emit namespace makes it possible to create an assembly and its modules, type definitions, and CIL implementation logic at runtime. Once you have done so, you are then free to save your in-memory binary to disk. This, of course, results in a new static assembly. To be sure, the process of building a dynamic assembly using the System.Reflection.Emit namespace does require some level of understanding regarding the nature of CIL opcodes.
Although creating dynamic assemblies is a fairly advanced (and uncommon) programming task, they can be useful under various circumstances:
Several aspects of the .NET runtime engine involve generating dynamic assemblies quietly in the background. For example, ASP.NET makes use of this technique to map markup and server-side script code into a runtime object model. LINQ also can generate code on the fly based on various query expressions. This being said, let’s check out the types within System.Reflection.Emit.
Creating a dynamic assembly requires you to have some familiarity with CIL opcodes, but the types of the System.Reflection.Emit namespace hide the complexity of CIL as much as possible. For example, rather than directly specifying the necessary CIL directives and attributes to define a class type, you can simply make use of the TypeBuilder class. Likewise, if you wish to define a new instance-level constructor, you have no need to emit the specialname, rtspecialname, or .ctor tokens; rather, you can make use of the ConstructorBuilder. Table 17-8 documents the key members of the System.Reflection.Emit namespace.
Table 17-8. Select Members of the System.Reflection.Emit Namespace
Members | Meaning in Life |
---|---|
AssemblyBuilder | Used to create an assembly (*.dll or *.exe) at runtime. *.exes must call the ModuleBuilder.SetEntryPoint() method to set the method that is the entry point to the module. If no entry point is specified, a *.dll will be generated. |
ModuleBuilder | Used to define the set of modules within the current assembly. |
EnumBuilder | Used to create a .NET enumeration type. |
TypeBuilder | May be used to create classes, interfaces, structures, and delegates within a module at runtime. |
MethodBuilder LocalBuilder PropertyBuilder FieldBuilder ConstructorBuilder CustomAttributeBuilder ParameterBuilder EventBuilder | Used to create type members (such as methods, local variables, properties, constructors, and attributes) at runtime. |
ILGenerator | Emits CIL opcodes into a given type member. |
OpCodes | Provides numerous fields that map to CIL opcodes. This type is used in conjunction with the various members of System.Reflection.Emit.ILGenerator. |
In general, the types of the System.Reflection.Emit namespace allow you to represent raw CIL tokens programmatically during the construction of your dynamic assembly. You will see many of these members in the example that follows; however, the ILGenerator type is worth checking out straightaway.
As its name implies, the ILGenerator type’s role is to inject CIL opcodes into a given type member. However, you cannot directly create ILGenerator objects, as this type has no public constructors, rather you receive an ILGenerator type by calling specific methods of the builder-centric types (such as the MethodBuilder and ConstructorBuilder types), for example:
// Obtain an ILGenerator from a ConstructorBuilder // object named "myCtorBuilder". ConstructorBuilder myCtorBuilder = new ConstructorBuilder(/* ...various args... */); ILGenerator myCILGen = myCtorBuilder.GetILGenerator();
Once you have an ILGenerator in your hands, you are then able to emit the raw CIL opcodes using any number of methods. Table 17-9 documents some (but not all) methods of ILGenerator.
Table 17-9. Various Methods of ILGenerator
Method | Meaning in Life |
BeginCatchBlock() | Begins a catch block |
BeginExceptionBlock() | Begins an exception block for a nonfiltered exception |
BeginFinallyBlock() | Begins a finally block |
BeginScope() | Begins a lexical scope |
DeclareLocal() | Declares a local variable |
DefineLabel() | Declares a new label |
Emit() | Is overloaded numerous times to allow you to emit CIL opcodes |
EmitCall() | Pushes a call or callvirt opcode into the CIL stream |
EmitWriteLine() | Emits a call to Console.WriteLine() with different types of values |
EndExceptionBlock() | Ends an exception block |
EndScope() | Ends a lexical scope |
ThrowException() | Emits an instruction to throw an exception |
UsingNamespace() | Specifies the namespace to be used in evaluating locals and watches for the current active lexical scope |
The key method of ILGenerator is Emit(), which works in conjunction with the System.Reflection.Emit.OpCodes class type. As mentioned earlier in this chapter, this type exposes a good number of read-only fields that map to raw CIL opcodes. The full set of these members are all documented within online help, and you will see various examples in the pages that follow.
To illustrate the process of defining a .NET assembly at runtime, let’s walk through the process of creating a single-file dynamic assembly named MyAssembly.dll. Within this module is a class named HelloWorld. The HelloWorld class supports a default constructor and a custom constructor that is used to assign the value of a private member variable (theMessage) of type string. In addition, HelloWorld supports a public instance method named SayHello(), which prints a greeting to the standard I/O stream, and another instance method named GetMsg(), which returns the internal private string. In effect, you are going to programmatically generate the following class type:
// This class will be created at runtime // using System.Reflection.Emit. public class HelloWorld { private string theMessage; HelloWorld() {} HelloWorld(string s) {theMessage = s;} public string GetMsg() {return theMessage;} public void SayHello() { System.Console.WriteLine("Hello from the HelloWorld class!"); } }
Assume you have created a new Visual Studio 2010 Console Application project workspace named DynamicAsmBuilder and import the System.Reflection, System.Reflection.Emit, and -System.Threading namespaces. Define a static method named CreateMyAsm(). This single method is in charge of the following:
Also note that the CreateMyAsm() method takes as a single parameter a System.AppDomain type, which will be used to obtain access to the AssemblyBuilder type associated with the current application domain (see Chapter 17 for a discussion of .NET application domains). Here is the complete code, with analysis to follow:
// The caller sends in an AppDomain type. public static void CreateMyAsm(AppDomain curAppDomain) { // Establish general assembly characteristics. AssemblyName assemblyName = new AssemblyName(); assemblyName.Name = "MyAssembly"; assemblyName.Version = new Version("1.0.0.0"); // Create new assembly within the current AppDomain. AssemblyBuilder assembly = curAppDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Save); // Given that we are building a single-file // assembly, the name of the module is the same as the assembly. ModuleBuilder module = assembly.DefineDynamicModule("MyAssembly", "MyAssembly.dll"); // Define a public class named "HelloWorld". TypeBuilder helloWorldClass = module.DefineType("MyAssembly.HelloWorld", TypeAttributes.Public); // Define a private String member variable named "theMessage". FieldBuilder msgField = helloWorldClass.DefineField("theMessage", Type.GetType("System.String"), FieldAttributes.Private); // Create the custom ctor. Type[] constructorArgs = new Type[1]; constructorArgs[0] = typeof(string); ConstructorBuilder constructor = helloWorldClass.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, constructorArgs); ILGenerator constructorIL = constructor.GetILGenerator(); constructorIL.Emit(OpCodes.Ldarg_0); Type objectClass = typeof(object); ConstructorInfo superConstructor = objectClass.GetConstructor(new Type[0]); constructorIL.Emit(OpCodes.Call, superConstructor); constructorIL.Emit(OpCodes.Ldarg_0); constructorIL.Emit(OpCodes.Ldarg_1); constructorIL.Emit(OpCodes.Stfld, msgField); constructorIL.Emit(OpCodes.Ret); // Create the default ctor. helloWorldClass.DefineDefaultConstructor(MethodAttributes.Public); // Now create the GetMsg() method. MethodBuilder getMsgMethod = helloWorldClass.DefineMethod("GetMsg", MethodAttributes.Public, typeof(string), null); ILGenerator methodIL = getMsgMethod.GetILGenerator(); methodIL.Emit(OpCodes.Ldarg_0); methodIL.Emit(OpCodes.Ldfld, msgField); methodIL.Emit(OpCodes.Ret); // Create the SayHello method. MethodBuilder sayHiMethod = helloWorldClass.DefineMethod("SayHello", MethodAttributes.Public, null, null); methodIL = sayHiMethod.GetILGenerator(); methodIL.EmitWriteLine("Hello from the HelloWorld class!"); methodIL.Emit(OpCodes.Ret); // "Bake" the class HelloWorld. // (Baking is the formal term for emitting the type) helloWorldClass.CreateType(); // (Optionally) save the assembly to file. assembly.Save("MyAssembly.dll"); }
The method body begins by establishing the minimal set of characteristics about your assembly, using the AssemblyName and Version types (defined in the System.Reflection namespace). Next, you obtain an AssemblyBuilder type via the instance-level AppDomain.DefineDynamicAssembly() method (recall the caller will pass in an AppDomain reference into the CreateMyAsm() method):
// Establish general assembly characteristics // and gain access to the AssemblyBuilder type. public static void CreateMyAsm(AppDomain curAppDomain) { AssemblyName assemblyName = new AssemblyName(); assemblyName.Name = "MyAssembly"; assemblyName.Version = new Version("1.0.0.0"); // Create new assembly within the current AppDomain. AssemblyBuilder assembly = curAppDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Save); ... }
As you can see, when calling AppDomain.DefineDynamicAssembly(), you must specify the access mode of the assembly you wish to define, which can be any of the values shown in Table 17-10.
Table 17-10. Values of the AssemblyBuilderAccess Enumeration
Value | Meaning in Life |
---|---|
ReflectionOnly | Represents that a dynamic assembly that can only be reflected over |
Run | Represents that a dynamic assembly can be executed in memory but not saved to disk |
RunAndSave | Represents that a dynamic assembly can be executed in memory and saved to disk |
Save | Represents that a dynamic assembly can be saved to disk but not executed in memory |
The next task is to define the module set for your new assembly. Given that the assembly is a single file unit, you need to define only a single module. If you were to build a multi-file assembly using the DefineDynamicModule() method, you would specify an optional second parameter that represents the name of a given module (e.g., myMod.dotnetmodule). However, when creating a single-file assembly, the name of the module will be identical to the name of the assembly itself. In any case, once the DefineDynamicModule() method has returned, you are provided with a reference to a valid ModuleBuilder type:
// The single-file assembly. ModuleBuilder module = assembly.DefineDynamicModule("MyAssembly", "MyAssembly.dll");
ModuleBuilder is the key type used during the development of dynamic assemblies. As you would expect, ModuleBuilder supports a number of members that allow you to define the set of types contained within a given module (classes, interfaces, structures, etc.) as well as the set of embedded resources (string tables, images, etc.) contained within. Table 17-11 describes a few of the creation-centric methods. (Do note that each method will return to you a related type that represents the type you wish to construct.)
Table 17-11. Select Members of the ModuleBuilder Type
Method | Meaning in Life |
---|---|
DefineEnum() | Used to emit a .NET enum definition |
DefineResource() | Defines a managed embedded resource to be stored in this module |
DefineType() | Constructs a TypeBuilder, which allows you to define value types, interfaces, and class types (including delegates) |
The key member of the ModuleBuilder class to be aware of is DefineType(). In addition to specifying the name of the type (via a simple string), you will also make use of the System.Reflection.TypeAttributes enum to describe the format of the type itself. Table 17-12 lists some (but not all) of the key members of the TypeAttributes enumeration.
Table 17-12. Select Members of the TypeAttributes Enumeration
Member | Meaning in Life |
---|---|
Abstract | Specifies that the type is abstract |
Class | Specifies that the type is a class |
Interface | Specifies that the type is an interface |
NestedAssembly | Specifies that the class is nested with assembly visibility and is thus accessible only by methods within its assembly |
NestedFamAndAssem | Specifies that the class is nested with assembly and family visibility, and is thus accessible only by methods lying in the intersection of its family and assembly |
NestedFamily | Specifies that the class is nested with family visibility and is thus accessible only by methods within its own type and any subtypes |
NestedFamORAssem | Specifies that the class is nested with family or assembly visibility, and is thus accessible only by methods lying in the union of its family and assembly |
NestedPrivate | Specifies that the class is nested with private visibility |
NestedPublic | Specifies that the class is nested with public visibility |
NotPublic | Specifies that the class is not public |
Public | Specifies that the class is public |
Sealed | Specifies that the class is concrete and cannot be extended |
Serializable | Specifies that the class can be serialized |
Now that you have a better understanding of the role of the ModuleBuilder.CreateType() method, let’s examine how you can emit the public HelloWorld class type and the private string variable:
// Define a public class named "MyAssembly.HelloWorld". TypeBuilder helloWorldClass = module.DefineType("MyAssembly.HelloWorld", TypeAttributes.Public); // Define a private String member variable named "theMessage". FieldBuilder msgField = helloWorldClass.DefineField("theMessage", typeof(string), FieldAttributes.Private);
Notice how the TypeBuilder.DefineField() method provides access to a FieldBuilder type. The TypeBuilder class also defines other methods that provide access to other “builder” types. For example, DefineConstructor() returns a ConstructorBuilder, DefineProperty() returns a PropertyBuilder, and so forth.
As mentioned earlier, the TypeBuilder.DefineConstructor() method can be used to define a -constructor for the current type. However, when it comes to implementing the constructor of -HelloClass, you need to inject raw CIL code into the constructor body, which is responsible for assigning the incoming parameter to the internal private string. To obtain an ILGenerator type, you call the GetILGenerator() method from the respective “builder” type you have reference to (in this case, the ConstructorBuilder type).
The Emit() method of the ILGenerator class is the entity in charge of placing CIL into a member implementation. Emit() itself makes frequent use of the OpCodes class type, which exposes the opcode set of CIL using read-only fields. For example, OpCodes.Ret signals the return of a method call. OpCodes.Stfld makes an assignment to a member variable. OpCodes.Call is used to call a given method (in this case, the base class constructor). That said, ponder the following constructor logic:
// Create the custom constructor taking // a single System.String argument. Type[] constructorArgs = new Type[1]; constructorArgs[0] = typeof(string); ConstructorBuilder constructor = helloWorldClass.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, constructorArgs); // Now emit the necessary CIL into the ctor. ILGenerator constructorIL = constructor.GetILGenerator(); constructorIL.Emit(OpCodes.Ldarg_0); Type objectClass = typeof(object); ConstructorInfo superConstructor = objectClass.GetConstructor(new Type[0]); constructorIL.Emit(OpCodes.Call, superConstructor); // Call base class ctor. // Load the object's "this" pointer on the stack. constructorIL.Emit(OpCodes.Ldarg_0); // Load incoming argument on virtual stack and store in msgField. constructorIL.Emit(OpCodes.Ldarg_1); constructorIL.Emit(OpCodes.Stfld, msgField); // Assign msgField. constructorIL.Emit(OpCodes.Ret); // Return.
Last but not least, let’s examine the process of emitting the SayHello() method. The first task is to obtain a MethodBuilder type from the helloWorldClass variable. Once you do this, you define the method and obtain the underlying ILGenerator to inject the CIL instructions:
// Create the SayHello method. MethodBuilder sayHiMethod = helloWorldClass.DefineMethod("SayHello", MethodAttributes.Public, null, null); methodIL = sayHiMethod.GetILGenerator(); // Write a line to the Console. methodIL.EmitWriteLine("Hello there!"); methodIL.Emit(OpCodes.Ret);
Here you have established a public method (MethodAttributes.Public) that takes no parameters and returns nothing (marked by the null entries contained in the DefineMethod() call). Also note the EmitWriteLine() call. This helper member of the ILGenerator class automatically writes a line to the standard output with minimal fuss and bother.
Now that you have the logic in place to create and save your assembly, all that’s needed is a class to trigger the logic. To come full circle, assume your current project defines a second class named AsmReader. The logic in Main() obtains the current AppDomain via the Thread.GetDomain() method that will be used to host the assembly you will dynamically create. Once you have a reference, you are able to call the CreateMyAsm() method.
To make things a bit more interesting, once the call to CreateMyAsm() returns, you will exercise some late binding (see Chapter 15) to load your newly created assembly into memory and interact with the members of the HelloWorld class. Update your Main() method as follows:
static void Main(string[] args) { Console.WriteLine("***** The Amazing Dynamic Assembly Builder App *****"); // Get the application domain for the current thread. AppDomain curAppDomain = Thread.GetDomain(); // Create the dynamic assembly using our helper f(x). CreateMyAsm(curAppDomain); Console.WriteLine("-> Finished creating MyAssembly.dll."); // Now load the new assembly from file. Console.WriteLine("-> Loading MyAssembly.dll from file."); Assembly a = Assembly.Load("MyAssembly"); // Get the HelloWorld type. Type hello = a.GetType("MyAssembly.HelloWorld"); // Create HelloWorld object and call the correct ctor. Console.Write("-> Enter message to pass HelloWorld class: "); string msg = Console.ReadLine(); object[] ctorArgs = new object[1]; ctorArgs[0] = msg; object obj = Activator.CreateInstance(hello, ctorArgs); // Call SayHello and show returned string. Console.WriteLine("-> Calling SayHello() via late binding."); MethodInfo mi = hello.GetMethod("SayHello"); mi.Invoke(obj, null); // Invoke method. mi = hello.GetMethod("GetMsg"); Console.WriteLine(mi.Invoke(obj, null)); }
In effect, you have just created a .NET assembly that is able to create and execute .NET assemblies at runtime! That wraps up the examination of CIL and the role of dynamic assemblies. I hope this chapter has deepened your understanding of the .NET type system and the syntax and semantics of CIL.
Source CodeThe DynamicAsmBuilder project is included under the Chapter 17 subdirectory